﻿/*global define:false */
/**
 * Copyright 2013 Craig Campbell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Mousetrap is a simple keyboard shortcut library for Javascript with
 * no external dependencies
 *
 * @version 1.4.6
 * @url craig.is/killing/mice
 */
(function (window, document, undefined) {

	/**
	 * mapping of special keycodes to their corresponding keys
	 *
	 * everything in this dictionary cannot use keypress events
	 * so it has to be here to map to the correct keycodes for
	 * keyup/keydown events
	 *
	 * @type {Object}
	 */
	var _MAP = {
		8: 'backspace',
		9: 'tab',
		13: 'enter',
		16: 'shift',
		17: 'ctrl',
		18: 'alt',
		20: 'capslock',
		27: 'esc',
		32: 'space',
		33: 'pageup',
		34: 'pagedown',
		35: 'end',
		36: 'home',
		37: 'left',
		38: 'up',
		39: 'right',
		40: 'down',
		45: 'ins',
		46: 'del',
		91: 'meta',
		93: 'meta',
		224: 'meta'
	},

			/**
			 * mapping for special characters so they can support
			 *
			 * this dictionary is only used incase you want to bind a
			 * keyup or keydown event to one of these keys
			 *
			 * @type {Object}
			 */
			_KEYCODE_MAP = {
				106: '*',
				107: '+',
				109: '-',
				110: '.',
				111: '/',
				186: ';',
				187: '=',
				188: ',',
				189: '-',
				190: '.',
				191: '/',
				192: '`',
				219: '[',
				220: '\\',
				221: ']',
				222: '\''
			},

			/**
			 * this is a mapping of keys that require shift on a US keypad
			 * back to the non shift equivelents
			 *
			 * this is so you can use keyup events with these keys
			 *
			 * note that this will only work reliably on US keyboards
			 *
			 * @type {Object}
			 */
			_SHIFT_MAP = {
				'~': '`',
				'!': '1',
				'@': '2',
				'#': '3',
				'$': '4',
				'%': '5',
				'^': '6',
				'&': '7',
				'*': '8',
				'(': '9',
				')': '0',
				'_': '-',
				'+': '=',
				':': ';',
				'\"': '\'',
				'<': ',',
				'>': '.',
				'?': '/',
				'|': '\\'
			},

			/**
			 * this is a list of special strings you can use to map
			 * to modifier keys when you specify your keyboard shortcuts
			 *
			 * @type {Object}
			 */
			_SPECIAL_ALIASES = {
				'option': 'alt',
				'command': 'meta',
				'return': 'enter',
				'escape': 'esc',
				'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
			},

			/**
			 * variable to store the flipped version of _MAP from above
			 * needed to check if we should use keypress or not when no action
			 * is specified
			 *
			 * @type {Object|undefined}
			 */
			_REVERSE_MAP,

			/**
			 * a list of all the callbacks setup via Mousetrap.bind()
			 *
			 * @type {Object}
			 */
			_callbacks = {},

			/**
			 * direct map of string combinations to callbacks used for trigger()
			 *
			 * @type {Object}
			 */
			_directMap = {},

			/**
			 * keeps track of what level each sequence is at since multiple
			 * sequences can start out with the same sequence
			 *
			 * @type {Object}
			 */
			_sequenceLevels = {},

			/**
			 * variable to store the setTimeout call
			 *
			 * @type {null|number}
			 */
			_resetTimer,

			/**
			 * temporary state where we will ignore the next keyup
			 *
			 * @type {boolean|string}
			 */
			_ignoreNextKeyup = false,

			/**
			 * temporary state where we will ignore the next keypress
			 *
			 * @type {boolean}
			 */
			_ignoreNextKeypress = false,

			/**
			 * are we currently inside of a sequence?
			 * type of action ("keyup" or "keydown" or "keypress") or false
			 *
			 * @type {boolean|string}
			 */
			_nextExpectedAction = false;

	/**
	 * loop through the f keys, f1 to f19 and add them to the map
	 * programatically
	 */
	for (var i = 1; i < 20; ++i) {
		_MAP[111 + i] = 'f' + i;
	}

	/**
	 * loop through to map numbers on the numeric keypad
	 */
	for (i = 0; i <= 9; ++i) {
		_MAP[i + 96] = i;
	}

	/**
	 * cross browser add event method
	 *
	 * @param {Element|HTMLDocument} object
	 * @param {string} type
	 * @param {Function} callback
	 * @returns void
	 */
	function _addEvent(object, type, callback) {
		if (object.addEventListener) {
			object.addEventListener(type, callback, false);
			return;
		}

		object.attachEvent('on' + type, callback);
	}

	/**
	 * takes the event and returns the key character
	 *
	 * @param {Event} e
	 * @return {string}
	 */
	function _characterFromEvent(e) {

		// for keypress events we should return the character as is
		if (e.type == 'keypress') {
			var character = String.fromCharCode(e.which);

			// if the shift key is not pressed then it is safe to assume
			// that we want the character to be lowercase.  this means if
			// you accidentally have caps lock on then your key bindings
			// will continue to work
			//
			// the only side effect that might not be desired is if you
			// bind something like 'A' cause you want to trigger an
			// event when capital A is pressed caps lock will no longer
			// trigger the event.  shift+a will though.
			if (!e.shiftKey) {
				character = character.toLowerCase();
			}

			return character;
		}

		// for non keypress events the special maps are needed
		if (_MAP[e.which]) {
			return _MAP[e.which];
		}

		if (_KEYCODE_MAP[e.which]) {
			return _KEYCODE_MAP[e.which];
		}

		// if it is not in the special map

		// with keydown and keyup events the character seems to always
		// come in as an uppercase character whether you are pressing shift
		// or not.  we should make sure it is always lowercase for comparisons
		return String.fromCharCode(e.which).toLowerCase();
	}

	/**
	 * checks if two arrays are equal
	 *
	 * @param {Array} modifiers1
	 * @param {Array} modifiers2
	 * @returns {boolean}
	 */
	function _modifiersMatch(modifiers1, modifiers2) {
		return modifiers1.sort().join(',') === modifiers2.sort().join(',');
	}

	/**
	 * resets all sequence counters except for the ones passed in
	 *
	 * @param {Object} doNotReset
	 * @returns void
	 */
	function _resetSequences(doNotReset) {
		doNotReset = doNotReset || {};

		var activeSequences = false,
				key;

		for (key in _sequenceLevels) {
			if (doNotReset[key]) {
				activeSequences = true;
				continue;
			}
			_sequenceLevels[key] = 0;
		}

		if (!activeSequences) {
			_nextExpectedAction = false;
		}
	}

	/**
	 * finds all callbacks that match based on the keycode, modifiers,
	 * and action
	 *
	 * @param {string} character
	 * @param {Array} modifiers
	 * @param {Event|Object} e
	 * @param {string=} sequenceName - name of the sequence we are looking for
	 * @param {string=} combination
	 * @param {number=} level
	 * @returns {Array}
	 */
	function _getMatches(character, modifiers, e, sequenceName, combination, level) {
		var i,
				callback,
				matches = [],
				action = e.type;

		// if there are no events related to this keycode
		if (!_callbacks[character]) {
			return [];
		}

		// if a modifier key is coming up on its own we should allow it
		if (action == 'keyup' && _isModifier(character)) {
			modifiers = [character];
		}

		// loop through all callbacks for the key that was pressed
		// and see if any of them match
		for (i = 0; i < _callbacks[character].length; ++i) {
			callback = _callbacks[character][i];

			// if a sequence name is not specified, but this is a sequence at
			// the wrong level then move onto the next match
			if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
				continue;
			}

			// if the action we are looking for doesn't match the action we got
			// then we should keep going
			if (action != callback.action) {
				continue;
			}

			// if this is a keypress event and the meta key and control key
			// are not pressed that means that we need to only look at the
			// character, otherwise check the modifiers as well
			//
			// chrome will not fire a keypress if meta or control is down
			// safari will fire a keypress if meta or meta+shift is down
			// firefox will fire a keypress if meta or control is down
			if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {

				// when you bind a combination or sequence a second time it
				// should overwrite the first one.  if a sequenceName or
				// combination is specified in this call it does just that
				//
				// @todo make deleting its own method?
				var deleteCombo = !sequenceName && callback.combo == combination;
				var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
				if (deleteCombo || deleteSequence) {
					_callbacks[character].splice(i, 1);
				}

				matches.push(callback);
			}
		}

		return matches;
	}

	/**
	 * takes a key event and figures out what the modifiers are
	 *
	 * @param {Event} e
	 * @returns {Array}
	 */
	function _eventModifiers(e) {
		var modifiers = [];

		if (e.shiftKey) {
			modifiers.push('shift');
		}

		if (e.altKey) {
			modifiers.push('alt');
		}

		if (e.ctrlKey) {
			modifiers.push('ctrl');
		}

		if (e.metaKey) {
			modifiers.push('meta');
		}

		return modifiers;
	}

	/**
	 * prevents default for this event
	 *
	 * @param {Event} e
	 * @returns void
	 */
	function _preventDefault(e) {
		if (e.preventDefault) {
			e.preventDefault();
			return;
		}

		e.returnValue = false;
	}

	/**
	 * stops propogation for this event
	 *
	 * @param {Event} e
	 * @returns void
	 */
	function _stopPropagation(e) {
		if (e.stopPropagation) {
			e.stopPropagation();
			return;
		}

		e.cancelBubble = true;
	}

	/**
	 * actually calls the callback function
	 *
	 * if your callback function returns false this will use the jquery
	 * convention - prevent default and stop propogation on the event
	 *
	 * @param {Function} callback
	 * @param {Event} e
	 * @returns void
	 */
	function _fireCallback(callback, e, combo, sequence) {

		// if this event should not happen stop here
		if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
			return;
		}

		if (callback(e, combo) === false) {
			_preventDefault(e);
			_stopPropagation(e);
		}
	}

	/**
	 * handles a character key event
	 *
	 * @param {string} character
	 * @param {Array} modifiers
	 * @param {Event} e
	 * @returns void
	 */
	function _handleKey(character, modifiers, e) {
		var callbacks = _getMatches(character, modifiers, e),
				i,
				doNotReset = {},
				maxLevel = 0,
				processedSequenceCallback = false;

		// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
		for (i = 0; i < callbacks.length; ++i) {
			if (callbacks[i].seq) {
				maxLevel = Math.max(maxLevel, callbacks[i].level);
			}
		}

		// loop through matching callbacks for this key event
		for (i = 0; i < callbacks.length; ++i) {

			// fire for all sequence callbacks
			// this is because if for example you have multiple sequences
			// bound such as "g i" and "g t" they both need to fire the
			// callback for matching g cause otherwise you can only ever
			// match the first one
			if (callbacks[i].seq) {

				// only fire callbacks for the maxLevel to prevent
				// subsequences from also firing
				//
				// for example 'a option b' should not cause 'option b' to fire
				// even though 'option b' is part of the other sequence
				//
				// any sequences that do not match here will be discarded
				// below by the _resetSequences call
				if (callbacks[i].level != maxLevel) {
					continue;
				}

				processedSequenceCallback = true;

				// keep a list of which sequences were matches for later
				doNotReset[callbacks[i].seq] = 1;
				_fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
				continue;
			}

			// if there were no sequence matches but we are still here
			// that means this is a regular match so we should fire that
			if (!processedSequenceCallback) {
				_fireCallback(callbacks[i].callback, e, callbacks[i].combo);
			}
		}

		// if the key you pressed matches the type of sequence without
		// being a modifier (ie "keyup" or "keypress") then we should
		// reset all sequences that were not matched by this event
		//
		// this is so, for example, if you have the sequence "h a t" and you
		// type "h e a r t" it does not match.  in this case the "e" will
		// cause the sequence to reset
		//
		// modifier keys are ignored because you can have a sequence
		// that contains modifiers such as "enter ctrl+space" and in most
		// cases the modifier key will be pressed before the next key
		//
		// also if you have a sequence such as "ctrl+b a" then pressing the
		// "b" key will trigger a "keypress" and a "keydown"
		//
		// the "keydown" is expected when there is a modifier, but the
		// "keypress" ends up matching the _nextExpectedAction since it occurs
		// after and that causes the sequence to reset
		//
		// we ignore keypresses in a sequence that directly follow a keydown
		// for the same character
		var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
		if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
			_resetSequences(doNotReset);
		}

		_ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
	}

	/**
	 * handles a keydown event
	 *
	 * @param {Event} e
	 * @returns void
	 */
	function _handleKeyEvent(e) {

		// normalize e.which for key events
		// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
		if (typeof e.which !== 'number') {
			e.which = e.keyCode;
		}

		var character = _characterFromEvent(e);

		// no character found then stop
		if (!character) {
			return;
		}

		// need to use === for the character check because the character can be 0
		if (e.type == 'keyup' && _ignoreNextKeyup === character) {
			_ignoreNextKeyup = false;
			return;
		}

		Mousetrap.handleKey(character, _eventModifiers(e), e);
	}

	/**
	 * determines if the keycode specified is a modifier key or not
	 *
	 * @param {string} key
	 * @returns {boolean}
	 */
	function _isModifier(key) {
		return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
	}

	/**
	 * called to set a 1 second timeout on the specified sequence
	 *
	 * this is so after each key press in the sequence you have 1 second
	 * to press the next key before you have to start over
	 *
	 * @returns void
	 */
	function _resetSequenceTimer() {
		clearTimeout(_resetTimer);
		_resetTimer = setTimeout(_resetSequences, 1000);
	}

	/**
	 * reverses the map lookup so that we can look for specific keys
	 * to see what can and can't use keypress
	 *
	 * @return {Object}
	 */
	function _getReverseMap() {
		if (!_REVERSE_MAP) {
			_REVERSE_MAP = {};
			for (var key in _MAP) {

				// pull out the numeric keypad from here cause keypress should
				// be able to detect the keys from the character
				if (key > 95 && key < 112) {
					continue;
				}

				if (_MAP.hasOwnProperty(key)) {
					_REVERSE_MAP[_MAP[key]] = key;
				}
			}
		}
		return _REVERSE_MAP;
	}

	/**
	 * picks the best action based on the key combination
	 *
	 * @param {string} key - character for key
	 * @param {Array} modifiers
	 * @param {string=} action passed in
	 */
	function _pickBestAction(key, modifiers, action) {

		// if no action was picked in we should try to pick the one
		// that we think would work best for this key
		if (!action) {
			action = _getReverseMap()[key] ? 'keydown' : 'keypress';
		}

		// modifier keys don't work as expected with keypress,
		// switch to keydown
		if (action == 'keypress' && modifiers.length) {
			action = 'keydown';
		}

		return action;
	}

	/**
	 * binds a key sequence to an event
	 *
	 * @param {string} combo - combo specified in bind call
	 * @param {Array} keys
	 * @param {Function} callback
	 * @param {string=} action
	 * @returns void
	 */
	function _bindSequence(combo, keys, callback, action) {

		// start off by adding a sequence level record for this combination
		// and setting the level to 0
		_sequenceLevels[combo] = 0;

		/**
		 * callback to increase the sequence level for this sequence and reset
		 * all other sequences that were active
		 *
		 * @param {string} nextAction
		 * @returns {Function}
		 */
		function _increaseSequence(nextAction) {
			return function () {
				_nextExpectedAction = nextAction;
				++_sequenceLevels[combo];
				_resetSequenceTimer();
			};
		}

		/**
		 * wraps the specified callback inside of another function in order
		 * to reset all sequence counters as soon as this sequence is done
		 *
		 * @param {Event} e
		 * @returns void
		 */
		function _callbackAndReset(e) {
			_fireCallback(callback, e, combo);

			// we should ignore the next key up if the action is key down
			// or keypress.  this is so if you finish a sequence and
			// release the key the final key will not trigger a keyup
			if (action !== 'keyup') {
				_ignoreNextKeyup = _characterFromEvent(e);
			}

			// weird race condition if a sequence ends with the key
			// another sequence begins with
			setTimeout(_resetSequences, 10);
		}

		// loop through keys one at a time and bind the appropriate callback
		// function.  for any key leading up to the final one it should
		// increase the sequence. after the final, it should reset all sequences
		//
		// if an action is specified in the original bind call then that will
		// be used throughout.  otherwise we will pass the action that the
		// next key in the sequence should match.  this allows a sequence
		// to mix and match keypress and keydown events depending on which
		// ones are better suited to the key provided
		for (var i = 0; i < keys.length; ++i) {
			var isFinal = i + 1 === keys.length;
			var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
			_bindSingle(keys[i], wrappedCallback, action, combo, i);
		}
	}

	/**
	 * Converts from a string key combination to an array
	 *
	 * @param  {string} combination like "command+shift+l"
	 * @return {Array}
	 */
	function _keysFromString(combination) {
		if (combination === '+') {
			return ['+'];
		}

		return combination.split('+');
	}

	/**
	 * Gets info for a specific key combination
	 *
	 * @param  {string} combination key combination ("command+s" or "a" or "*")
	 * @param  {string=} action
	 * @returns {Object}
	 */
	function _getKeyInfo(combination, action) {
		var keys,
				key,
				i,
				modifiers = [];

		// take the keys from this pattern and figure out what the actual
		// pattern is all about
		keys = _keysFromString(combination);

		for (i = 0; i < keys.length; ++i) {
			key = keys[i];

			// normalize key names
			if (_SPECIAL_ALIASES[key]) {
				key = _SPECIAL_ALIASES[key];
			}

			// if this is not a keypress event then we should
			// be smart about using shift keys
			// this will only work for US keyboards however
			if (action && action != 'keypress' && _SHIFT_MAP[key]) {
				key = _SHIFT_MAP[key];
				modifiers.push('shift');
			}

			// if this key is a modifier then add it to the list of modifiers
			if (_isModifier(key)) {
				modifiers.push(key);
			}
		}

		// depending on what the key combination is
		// we will try to pick the best event for it
		action = _pickBestAction(key, modifiers, action);

		return {
			key: key,
			modifiers: modifiers,
			action: action
		};
	}

	/**
	 * binds a single keyboard combination
	 *
	 * @param {string} combination
	 * @param {Function} callback
	 * @param {string=} action
	 * @param {string=} sequenceName - name of sequence if part of sequence
	 * @param {number=} level - what part of the sequence the command is
	 * @returns void
	 */
	function _bindSingle(combination, callback, action, sequenceName, level) {

		// store a direct mapped reference for use with Mousetrap.trigger
		_directMap[combination + ':' + action] = callback;

		// make sure multiple spaces in a row become a single space
		combination = combination.replace(/\s+/g, ' ');

		var sequence = combination.split(' '),
				info;

		// if this pattern is a sequence of keys then run through this method
		// to reprocess each pattern one key at a time
		if (sequence.length > 1) {
			_bindSequence(combination, sequence, callback, action);
			return;
		}

		info = _getKeyInfo(combination, action);

		// make sure to initialize array if this is the first time
		// a callback is added for this key
		_callbacks[info.key] = _callbacks[info.key] || [];

		// remove an existing match if there is one
		_getMatches(info.key, info.modifiers, { type: info.action }, sequenceName, combination, level);

		// add this call back to the array
		// if it is a sequence put it at the beginning
		// if not put it at the end
		//
		// this is important because the way these are processed expects
		// the sequence ones to come first
		_callbacks[info.key][sequenceName ? 'unshift' : 'push']({
			callback: callback,
			modifiers: info.modifiers,
			action: info.action,
			seq: sequenceName,
			level: level,
			combo: combination
		});
	}

	/**
	 * binds multiple combinations to the same callback
	 *
	 * @param {Array} combinations
	 * @param {Function} callback
	 * @param {string|undefined} action
	 * @returns void
	 */
	function _bindMultiple(combinations, callback, action) {
		for (var i = 0; i < combinations.length; ++i) {
			_bindSingle(combinations[i], callback, action);
		}
	}

	// start!
	_addEvent(document, 'keypress', _handleKeyEvent);
	_addEvent(document, 'keydown', _handleKeyEvent);
	_addEvent(document, 'keyup', _handleKeyEvent);

	var Mousetrap = {

		/**
		 * binds an event to mousetrap
		 *
		 * can be a single key, a combination of keys separated with +,
		 * an array of keys, or a sequence of keys separated by spaces
		 *
		 * be sure to list the modifier keys first to make sure that the
		 * correct key ends up getting bound (the last key in the pattern)
		 *
		 * @param {string|Array} keys
		 * @param {Function} callback
		 * @param {string=} action - 'keypress', 'keydown', or 'keyup'
		 * @returns void
		 */
		bind: function (keys, callback, action) {
			keys = keys instanceof Array ? keys : [keys];
			_bindMultiple(keys, callback, action);
			return this;
		},

		/**
		 * unbinds an event to mousetrap
		 *
		 * the unbinding sets the callback function of the specified key combo
		 * to an empty function and deletes the corresponding key in the
		 * _directMap dict.
		 *
		 * TODO: actually remove this from the _callbacks dictionary instead
		 * of binding an empty function
		 *
		 * the keycombo+action has to be exactly the same as
		 * it was defined in the bind method
		 *
		 * @param {string|Array} keys
		 * @param {string} action
		 * @returns void
		 */
		unbind: function (keys, action) {
			return Mousetrap.bind(keys, function () { }, action);
		},

		/**
		 * triggers an event that has already been bound
		 *
		 * @param {string} keys
		 * @param {string=} action
		 * @returns void
		 */
		trigger: function (keys, action) {
			if (_directMap[keys + ':' + action]) {
				_directMap[keys + ':' + action]({}, keys);
			}
			return this;
		},

		/**
		 * resets the library back to its initial state.  this is useful
		 * if you want to clear out the current keyboard shortcuts and bind
		 * new ones - for example if you switch to another page
		 *
		 * @returns void
		 */
		reset: function () {
			_callbacks = {};
			_directMap = {};
			return this;
		},

		/**
		 * should we stop this event before firing off callbacks
		 *
		 * @param {Event} e
		 * @param {Element} element
		 * @return {boolean}
		 */
		stopCallback: function (e, element) {

			// if the element has the class "mousetrap" then no need to stop
			if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
				return false;
			}

			// stop for input, select, and textarea
			return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
		},

		/**
		 * exposes _handleKey publicly so it can be overwritten by extensions
		 */
		handleKey: _handleKey
	};

	// expose mousetrap to the global object
	window.Mousetrap = Mousetrap;

	// expose mousetrap as an AMD module
	if (typeof define === 'function' && define.amd) {
		define(Mousetrap);
	}
})(window, document);